import operator
import math

class plane(object):
    """3d plane class, supports vector and scalar operators,
       and also provides a bunch of high level functions
       """
    __slots__ = ['dir', 'dip']
      
    def __init__(self, dir_or_pair, dip = None):
        if dip == None:
            self.dir, self.dip = self.dirdip(dir_or_pair[0], dir_or_pair[1])
        else:
            self.dir, self.dip = self.dirdip(dir_or_pair, dip)   
            
    def __len__(self):
        return 2
        
    def __getitem__(self, key):
        if key == 0:
            return self.dir
        elif key == 1:
            return self.dip
        else:
            raise IndexError("Invalid subscript "+str(key)+" to a plane")
            
    def __setitem__(self, key, value):
        if key == 0:
            self.dir, self.dip = self.dirdip(value, self.dip)
        elif key == 1:
            self.dir, self.dip = self.dirdip(self.dir, value)
        else:
            raise IndexError("Invalid subscript "+str(key)+" to a plane")

                
    # String representaion (for debugging)
    def __repr__(self):
        return '(dir:%s, dip:%s)' % (self.dir, self.dip)
        
    # Comparison
    def __eq__(self, other):
        if hasattr(other, "__getitem__") and len(other) == 2:
            return (self.dir, self.dip) == (self.dirdip (other[0], other[1]))
        else:
            return False
    
    def __ne__(self, other):
        if hasattr(other, "__getitem__") and len(other) == 2:
            return (self.dir, self.dip) != (self.dirdip (other[0], other[1]))
        else:
            return True
 
    def __nonzero__(self):
        return bool(self.dir or self.dip) 
    
    # Generic operator handlers
    def _o2(self, other, f):
        "Any two-operator operation where the left operand is a plane"
        if isinstance(other, plane):
            return plane(f(self.dir, other.dip),
                         f(self.dir, other.dip))
        elif (hasattr(other, "__getitem__")):
            return plane(f(self.dir, other[0]),
                         f(self.dip, other[1]))
        else:
            return plane(f(self.dir, other),
                         f(self.dip, other))
 
    def _r_o2(self, other, f):
        "Any two-operator operation where the right operand is a plane"
        if (hasattr(other, "__getitem__")):
            return plane(f(other[0], self.dir),
                         f(other[1], self.dip))
        else:
            return plane(f(other, self.dir),
                         f(other, self.dip))
 
    def _io(self, other, f):
        "inplace operator"
        if (hasattr(other, "__getitem__")):
            self.dir = f(self.dir, other[0])
            self.dip = f(self.dip, other[1])
        else:
            self.dir = f(self.dir, other)
            self.dip = f(self.dip, other)
        return self
    
    # Addition
    def __add__(self, other):
        if isinstance(other, plane):            
            return plane(self.dirdip((self.dir + other.dir) , (self.dip + other.dip)))
        elif hasattr(other, "__getitem__"):
            return plane(self.dirdip((self.dir + other[0]), (self.dip + other[1])))
        else:
            return plane(self.dirdip((self.dir + other), (self.dip + other)))
    __radd__ = __add__
    
    def __iadd__(self, other):
        if isinstance(other, plane):
            self.dir, self.dip = self.dirdip((self.dir + other.dir), (self.dip + other.dip))            
        elif hasattr(other, "__getitem__"):
            self.dir, self.dip = self.dirdip((self.dir + other[0]), (self.dip + other[1]))
        else:
            self.dir, self.dip = self.dirdip((self.dir + other), (self.dip + other))
        return self
        
    # Subtraction
    def __sub__(self, other):
        if isinstance(other, plane):            
            return plane(self.dirdip((self.dir - other.dir) , (self.dip - other.dip)))
        elif hasattr(other, "__getitem__"):
            return plane(self.dirdip((self.dir - other[0]), (self.dip - other[1])))
        else:
            return plane(self.dirdip((self.dir - other), (self.dip - other)))
    def __rsub__(self, other):
        if isinstance(other, plane):            
            return plane(self.dirdip((other.dir - self.dir) , (other.dip - self.dip)))
        elif hasattr(other, "__getitem__"):
            return plane(self.dirdip((other[0] - self.dir), (other[1] - self.dip)))
        else:
            return plane(self.dirdip((other - self.dir), (other - self.dip)))
    def __isub__(self, other):
        if isinstance(other, plane):
            self.dir, self.dip = self.dirdip((self.dir - other.dir), (self.dip - other.dip))            
        elif hasattr(other, "__getitem__"):
            self.dir, self.dip = self.dirdip((self.dir - other[0]), (self.dip - other[1]))
        else:
            self.dir, self.dip = self.dirdip((self.dir - other), (self.dip - other))
        return self
        
    # Multiplication
    def __mul__(self, other):
        if isinstance(other, plane):
            return plane(self.dirdip((self.dir*other.dir), (self.dip*other.dip)))
        if (hasattr(other, "__getitem__")):
            return plane(self.dirdip((self.dir*other[0]), (self.dip*other[1])))
        else:
            return plane(self.dirdip((self.dir*other), (self.dip*other)))
    __rmul__ = __mul__
    
    def __imul__(self, other):
        if isinstance(other, plane):
            self.dir, self.dip = self.dirdip((self.dir * other.dir), (self.dip * other.dip))
        elif hasattr(other, "__getitem__"):
            self.dir, self.dip = self.dirdip((self.dir * other[0]), (self.dip * other[1]))
        else:
            self.dir, self.dip = self.dirdip((self.dir * other), (self.dip * other))
        return self
     
    # Division
    def __div__(self, other):
        return self._o2(other, operator.div)
    def __rdiv__(self, other):
        return self._r_o2(other, operator.div)
    def __idiv__(self, other):
        return self._io(other, operator.div)
 
    def __floordiv__(self, other):
        return self._o2(other, operator.floordiv)
    def __rfloordiv__(self, other):
        return self._r_o2(other, operator.floordiv)
    def __ifloordiv__(self, other):
        return self._io(other, operator.floordiv)
 
    def __truediv__(self, other):
        return self._o2(other, operator.truediv)
    def __rtruediv__(self, other):
        return self._r_o2(other, operator.truediv)
    def __itruediv__(self, other):
        return self._io(other, operator.floordiv)
        
    # Modulo
    def __mod__(self, other):
        return self._o2(other, operator.mod)
    def __rmod__(self, other):
        return self._r_o2(other, operator.mod)
 
    def __divmod__(self, other):
        return self._o2(other, operator.divmod)
    def __rdivmod__(self, other):
        return self._r_o2(other, operator.divmod)
 
    ## Exponentation
    #def __pow__(self, other):
    #    return self._o2(other, operator.pow)
    #def __rpow__(self, other):
    #   return self._r_o2(other, operator.pow)
 
    # Bitwise operators
    def __lshift__(self, other):
        return self._o2(other, operator.lshift)
    def __rlshift__(self, other):
        return self._r_o2(other, operator.lshift)
 
    def __rshift__(self, other):
        return self._o2(other, operator.rshift)
    def __rrshift__(self, other):
        return self._r_o2(other, operator.rshift)
 
    def __and__(self, other):
        return self._o2(other, operator.and_)
    __rand__ = __and__
 
    def __or__(self, other):
        return self._o2(other, operator.or_)
    __ror__ = __or__
 
    def __xor__(self, other):
        return self._o2(other, operator.xor)
    __rxor__ = __xor__
    
    # Unary operations
    def __neg__(self):
        return plane(operator.neg(self.dir), operator.neg(self.dip))
 
    def __pos__(self):
        return plane(operator.pos(self.dir), operator.pos(self.dip))
 
    def __abs__(self):
        return plane(abs(self.dir), abs(self.dip))
 
    def __invert__(self):
        return plane(-self.dir, -self.dip)        

    # Cos-sin operations
    def get_cos1(self):
        return math.sin(math.radians(self.dir)) * math.cos(math.radians(self.dip))
    def __setcos1(self, val):
        pass
    cos1 = property(get_cos1, __setcos1, None, "gets or sets the guide cos of the plane")
    
    def get_cos2(self):
        return math.sin(math.radians(self.dip))
    def __setcos2(self, val):
        pass
    cos2 = property(get_cos2, __setcos2, None, "gets or sets the guide cos of the plane")
    
    def get_cos3(self):
        return math.cos(math.radians(self.dir)) * math.cos(math.radians(self.dip))
    def __setcos3(self, val):
        pass
    cos3 = property(get_cos3, __setcos3, None, "gets or sets the guide cos of the plane")
    
    def define_by_cos(self, cos1_or_three, cos2 = None, cos3 = None):
        if cos2 == None:
            if cos1_or_three[0]**2 + cos1_or_three[1]**2 + cos1_or_three[2]**2 <0.9999:
                raise ValueError ("cos1**2 + cos2**2 + cos3**3 must be equal to 1")
            dp = math.degrees( math.asin(cos1_or_three[1]) )
            try:
                dr = math.degrees( math.asin(cos1_or_three[0] / math.sqrt(1 - cos1_or_three[1]**2)))
            except ZeroDivisionError:
                dr = 0
        else:
            if cos1_or_three**2 + cos2**2 + cos3**2 <0.9999:
                raise ValueError ("cos1**2 + cos2**2 + cos3**3 must be equal to 1")
            dp = math.degrees( math.asin(cos2) )
            try:
                dr = math.degrees( math.asin(cos1_or_three / math.sqrt(1 - cos2**2)))
            except ZeroDivisionError:
                dr = 0
        self.dir, self.dip = self.dirdip(dr, dp)
    
    # Vector operations
    def get_angle_between(self, other):
        return math.degrees(math.acos(self.cos1 * other.cos1 + \
                               self.cos2 * other.cos2 + self.cos3 * other.cos3))
    
    def get_perpendicular_between(self, other):
        result = plane(0, 0)        
        result.define_by_cos([self.cos2 * other.cos3 - self.cos3 * other.cos2, \
                self.cos3 * other.cos1 - self.cos1 * other.cos3, \
                self.cos1 * other.cos2 - self.cos2 * other.cos1])
        return result
        
    
    
    # maintenance for performe stayning of dip dir in range of (0 - 360), (0 - 90)
    def dirdip(self, dr, dp):
        dr = dr % 360
        dp = dp % 180
        if dp > 90:
            dr = (dr + 180) % 360
            dp = 180 - dp
        return dr, dp    

def main():
    a = plane (0, 0)
    b = plane (0, 0)
    for ddir in range(0, 360, 30):
        for ddip in range(0, 90, 30):
            a = plane(ddir, ddip)
            print "%03d %02d \t %03d " % \
            (a.dir, a.dip, a.get_angle_between(b))            
    a = plane (0,0)
    b = plane (0, 90)
    print a, b, a.get_perpendicular_between(b)    
        
if __name__ == "__main__": main()
